iT邦幫忙

2025 iThome 鐵人賽

DAY 20
2
Software Development

Playwright 玩家攻略:從新手村到魔王關系列 第 20

Day 20:撰寫你自己的魔導書|Page Object Models 設計模式

  • 分享至 

  • xImage
  •  

歷經一連串初階副本任務,我們已經習得不少進階的實戰技巧。接著,將迎來新的挑戰,認識 Page Object Models (以下簡稱 POM)。透過建立專屬的 POM,不僅能讓測試流程更加結構化,還能大幅提升程式的可讀性與維護性,讓我們的測試更加井然有序。

為什麼需要 Page Object Models?

隨著測試案例逐漸增加,往往會出現大量重複的頁面操作與定位邏輯。每當編寫新的測試案例時,似乎都得從頭再來一遍。而一旦頁面有所調整 ── 無論是文字修改,還是樣式變更 ── 都必須在浩瀚的測試碼中逐一修正,既繁瑣耗時又容易出錯。因此,藉由 POM 將頁面操作抽象化,並集中管理元素與行為,我們能讓測試碼更精簡、更清晰,同時具備更高的延展性與維護性。

什麼是 Page Object Models?

POM 是一種設計模式,它的概念是將頁面視為物件,在物件裡定義這個頁面的元素定位 (Locator)操作方法 (actions),如此一來,在編寫測試時,就不須再注意頁面細節,只需要呼叫事先定義好的物件方法,專注於測試流程,順利完成測試情境,而元素或操作有異動時,也只需修改物件即可。

如何撰寫 Page Object Models?

實作 POM 的步驟:

  1. 建立頁面類別:每個網頁或功能頁都對應一個類別。
  2. 定義元素定位:在類別中宣告該頁面需要操作的元素。
  3. 封裝操作方法:把常見的操作行為(例如登入、搜尋、輸入資料)寫成方法。
  4. 在測試中使用:測試時只需呼叫方法,就能完成對應操作。

只須按照上方步驟,就能一步步建立自己的專屬 POM。

尚未建立 Page Object Models 的測試

以撰寫 TypeScript 官網的測試為例,先來看看沒有定義 POM 的狀況:

import { test, expect } from '@playwright/test';

test.describe('TS Website', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('https://www.typescriptlang.org/');
  });

  // handbook page 第 1 個測試:前往 handbook 裡的 The Basics
  test('handbook page - has title', async ({ page }) => {
    // 點擊 handbook tab,並驗證標題
    await page.locator('#tab3').click();
    await expect(page.getByRole('heading', { name: 'The TypeScript Handbook' })).toBeVisible();

    // 點擊 sidebar 裡的 The Basics,並驗證標題
    await page.locator('nav[id="sidebar"]').getByRole('link', { name: 'The Basics' }).click();
    await expect(page.getByRole('heading', { name: 'The Basics', exact: true })).toBeVisible();

    // 接續其他測試...
  });

  // handbook page 第 2 個測試:前往 handbook 裡的 Narrowing
  test('handbook page - via npm', async ({ page }) => {
    // 點擊 handbook tab,並驗證標題
    await page.locator('#tab3').click();
    await expect(page.getByRole('heading', { name: 'The TypeScript Handbook' })).toBeVisible();

    // 點擊 sidebar 裡的 Narrowing,並驗證標題
    await page.locator('nav[id="sidebar"]').getByRole('link', { name: 'Narrowing' }).click();
    await expect(page.getByRole('heading', { name: 'Narrowing', exact: true })).toBeVisible();

    // 接續其他測試...
  });

  // handbook page 第 3 個測試:前往 handbook 裡的 More on Functions
  test('handbook page - has header', async ({ page }) => {
    // 點擊 handbook tab,並驗證標題
    await page.locator('#tab3').click();
    await expect(page.getByRole('heading', { name: 'The TypeScript Handbook' })).toBeVisible();

    // 點擊 sidebar 裡的 More on Functions,並驗證標題
    await page.locator('nav[id="sidebar"]').getByRole('link', { name: 'More on Functions' }).click();
    await expect(page.getByRole('heading', { name: 'More on Functions', exact: true })).toBeVisible();

    // 接續其他測試...
  });
});

可以看到這 3 個案例一直在重複:

  • 點擊 handbook 連結 → 驗證 handbook 頁面標題 → 點擊 sidebar 的特定章節連結 → 驗證章節頁面標題

如果後續有更多 handbook 頁面的測試,就會一直重複上面相同的步驟,這時,就是建立 POM 的時候了。

一步步建立 Page Object Models

Playwright 同時提供 TypeScript 與 JavaScript 的 POM 方法,這裡先以 TypeScript 建立 POM 的方式為主,JavaScript 的步驟相同,唯獨語法稍有不同,如有需要請參考 Playwright 官網 Page object models 的介紹

  1. 建立頁面類別:先建立一個資料夾統一管理 POM,接著在資料夾底下建立一個管理 Handbook 頁面的檔案,在檔案內先針對 TypeScript Handbook 建立一個 Page 物件。

    // pages/HandbookPage.ts
    import { Page, Locator, expect } from '@playwright/test';
    
    export class HandbookPage {
    readonly page: Page;
    readonly tabHandbook: Locator;
    readonly sidebar: Locator;
    
    constructor(page: Page) {
      this.page = page;
    }
    
  2. 定義元素定位:在 constructor 內加入元素定位

    constructor(page: Page) {
        this.page = page;
    
        // 加入元素定位
        this.handbook = page.locator('#tab3');
        this.sidebar = page.locator('nav[id="sidebar"]');
      }
    
  3. 封裝操作方法:在 class 裡將常用操作封裝起來

    export class HandbookPage {
      readonly page: Page;
      readonly handbook: Locator;
      readonly sidebar: Locator;
    
      constructor(page: Page) {
        this.page = page;
        this.handbook = page.locator('#tab3');
        this.sidebar = page.locator('nav[id="sidebar"]');
      }
    
      // 開啟 handbook 入口並同時驗證頁面已開啟
      async openHandbook(): Promise<void> {
        await this.handbook.click();
      }
    
      // 取得目標章節的連結 locator(方便被外部調用)
      getTopicLink(topic: string): Locator {
        return this.sidebar.getByRole('link', { name: topic });
      }
    
      // 取得標題 locator(讓測試可以做斷言)
      getHeadingLocator(topic: string): Locator {
        return this.page.getByRole('heading', { name: topic, exact: true });
      }
    
      // 斷言:標題可見(封裝常用驗證)
      async expectHeadingVisible(topic: string): Promise<void> {
        await expect(this.getHeadingLocator(topic)).toBeVisible();
      }
    
      // 進入指定章節頁面(包含點擊與等待頁面顯示)
      async goToTopic(topic: string): Promise<void> {
        await this.handbook.click();
        await this.page.waitForLoadState('load');
        await this.expectHeadingVisible('The TypeScript Handbook');
        const link = this.getTopicLink(topic);
        await link.click();
      }
    }
    
  4. 在測試中使用:引入建立的 POM,宣告Page Object 實例,並且在每個測試前都建立新的 Page Object 實例,再呼叫 goToTopic() 將操作流程替換

     import { test } from '@playwright/test';
     import { HandbookPage } from './pages/HandbookPages';
    
     test.describe('TS Website', () => {
       // 宣告Page Object 實例
       let handbookPage: HandbookPage;
    
       test.beforeEach(async ({ page }) => {
         await page.goto('https://www.typescriptlang.org/');
         // 在每個測試前都建立新的 Page Object 實例  
         handbookPage = new HandbookPage(page);
       });
    
       test('handbook page - has title', async ({ page }) => {
         // 前往 handbook page 的 The Basics,並驗證標題
         await handbookPage.goToTopic('The Basics');
    
         // 接續其他測試...
       });
    
       test('handbook page - via npm', async ({ page }) => {
         // 前往 handbook page 的 Narrowing,並驗證標題
         await handbookPage.goToTopic('Narrowing');
    
         // 接續其他測試...
       });
    
       test('handbook page - has header', async ({ page }) => {
         // 前往 handbook page 的 More on Functions,並驗證標題
         await handbookPage.goToTopic('More on Functions');
    
         // 接續其他測試...
       });
     });
    

這樣一來,就完成了 POM 的建立流程,從頁面類別的設計、元素定位的集中管理,到操作方法的封裝與最後在測試中呼叫使用,整個 POM 的開發與應用流程已經走完一輪。


到這裡,我們學會了如何建立自己的 Page Object Models (POM),在測試案例中成功應用。透過這種設計方式,不僅讓測試程式碼的可讀性可維護性大幅提升,也為後續的自動化流程打下了穩固基礎。接下來,我們要邁入自動化測試中最關鍵的一步,讓測試能夠在 CI/CD 環境中自動執行。


上一篇
Day 19:戰鬥場景轉換|多分頁/視窗處理技巧
下一篇
Day 21:自動化基地 (一)|在 GitHub Actions 上建立 CI/CD 流程
系列文
Playwright 玩家攻略:從新手村到魔王關24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言